

Wenzi
首页
归档
标签
微说
关于
如何实现 axios 的自定义适配器 adapter
蚊子前端博客
发布于 2020/10/22 10:43
我们想基于 axios 扩展一些自己的数据请求方式（例如 mock 数据，某些 APP 内专属的数据请求方式等），并能够使用上 axios 提供的便捷功能，该怎么自定义一个适配器 adapter
Axios 是一个非常优秀的基于 promise 的 HTTP 库，可以用在浏览器和 node.js 中。并且提供了很多便捷的功能，例如：

支持 Promise API
拦截请求和响应
转换请求数据和响应数据
取消请求
自动转换 JSON 数据
客户端支持防御 XSRF
但如果我们想基于 axios 扩展一些自己的数据请求方式（例如 mock 数据，某些 APP 内专属的数据请求方式等），并能够使用上 axios 提供的便捷功能，该怎么自定义一个适配器 adapter；

1. 适配器要实现的功能
我们在基于 axios 实现额外的数据模块时，应当与 axios 的模式进行对齐。因此在返回的数据格式上，实现的功能上尽量保持一致。

1.1 promise 和工具
所有的适配均应当实现为 Promise 方式。

而且，有些功能的实现，axios 将其下放到了适配器中自己进行实现，例如

url 的拼接：即 baseURL 和 url 的拼接，若存在 baseURL 且 url 为相对路径，则进行拼接，否则直接使用 url；
参数的拼接：若是 get 请求，需要自行将 object 类型拼接为 url 参数的格式并与 url 拼接完成；
这是自己需要实现的两个基本的工具方法。

1.2 响应的格式
这里我们要注意到请求接口正常和异常的格式。

接口正常时：

const result = {
    status: 200, // 接口的http 状态
    statusText: 'ok',
    config: 'config', // 传入的config配置，原样返回即可，方便在响应拦截器和响应结果中使用
    data: {}, // 真实的接口返回结果
};
接口异常时，我们可以看下 axios 源码中对错误信息的处理createError，enhanceError（createError 中调用了 enhanceError），首先会创建一个 error 实例，然后给这个 error 实例添加一个属性：

module.exports = function enhanceError(error, config, code, request, response) {
    error.config = config;
    if (code) {
        error.code = code;
    }

    error.request = request;
    error.response = response;
    error.isAxiosError = true;

    error.toJSON = function toJSON() {
        return {
            // Standard
            message: this.message,
            name: this.name,
            // Microsoft
            description: this.description,
            number: this.number,
            // Mozilla
            fileName: this.fileName,
            lineNumber: this.lineNumber,
            columnNumber: this.columnNumber,
            stack: this.stack,
            // Axios
            config: this.config,
            code: this.code,
        };
    };
    return error;
};
可以看到，除了正常的错误信息外，还加入了很多别的属性，例如 request, response, config 等。这里我们在自己实现适配器时，最好也要这样统一编写，方便更上层的业务层统一处理，避免为单独的适配器进行特殊处理。

关于 1.1 和 1.2 中的内容，若不进行打包编译，则需要自己实现。若还要通过 webpack 等打包工具编译一下的，可以直接引用 axios 中的方法，不用自己实现了，参考官方基于 axios 实现的mock-axios。例如：

import axios from 'axios';
import buildURL from 'axios/lib/helpers/buildURL';
import isURLSameOrigin from 'axios/lib/helpers/isURLSameOrigin';
import btoa from 'axios/lib/helpers/btoa';
import cookies from 'axios/lib/helpers/cookies';
import settle from 'axios/lib/core/settle';
import createError from 'axios/lib/core/createError';
然后直接使用就行了，不用再进行二次开发。

1.3 超时设置
我们不能无限地等待第三方服务的响应，如果第三方服务无响应或者响应时间过长，应当适时的终止掉。在 axios 中，前端使用了XMLHttpRequest，在 node 端使用了http，来实现接口的请求，两者都有超时的设定，可以设置 timeout 字段来设置超时的时间，自动取消当前的请求。

像有的发起的请求，自己并没有超时的设定，例如 jsonp，是用创建一个 script 标签来发起的请求，这个请求必须等到服务器有响应才会终止（成功或者失败）。这时，就需要我们自己用一个setTimeout来模拟了，但这样，即使返回给业务层说“超时了，已取消当前请求”，但实际上请求还在，只不过若超过规定时间，只是不再执行对应的成功操作而已。

1.4 主动取消请求
我们也会有很多并没有到超时时间，就需要主动取消当前请求的场景，例如在请求返回之前就切换了路由；上次请求还没响应前，又需要发出新的请求等。都需要主动地取消当前请求。

axios 中已经提供了取消请求的功能，我们只需要按照规则接入即可。我们来看下 XMLHttpRequest 请求器中是怎么取消请求的，在写自定义请求器时也可以照理使用。

在lib/adapters/xhr.js#L158中：

// 若config中已经配置了cancelToken
if (config.cancelToken) {
    // Handle cancellation
    // 若在外城执行了取消请求的方法，则这里将当前的请求取消掉
    config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
            return;
        }

        // xhr中使用abort方法取消当前请求
        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
    });
}
我们在写自己的适配器时，也可以将这段拷贝过去，将内部取消的操作更换为自己的即可。

关于 cancel 操作执行的原理，这里暂不展开，有兴趣的可以参考这篇文章：axios cancelToken 原理解析。

到这里，若把上面的功能都实现了，就已经完成了一个标准的适配器了。

2. 编写自定义适配器
每个人需要的适配器肯定也不一样，复杂度也不一样，例如有的想接入小程序的请求，我自己想接入客户端里提供的数据请求方式等。我们这里只是通过实现一个简单的jsonp适配器来讲解下实现方式。

我们以 es6 的模块方式来进行开发。所有的实现均在代码中进行了讲解。

// 这里的config是axios里所有的配置
const jsonpAdapter = (config) => {
    return new Promise((resolve, reject) => {
        // 是否已取消当前操作
        // 因jsonp没有主动取消请求的方式
        // 这里使用 isAbort 来标识
        let isAbort = false;

        // 定时器标识符
        let timer = null;

        // 执行方法的名字，
        const callbackName = `jsonp${Date.now()}_${Math.random()
            .toString()
            .slice(2)}`;

        // 这里假设已经实现了baseURL和url的拼接方法
        const fullPath = buildFullPath(config.baseURL, config.url);

        // 这里假设已经实现了url和参数的拼接方法
        // 不太一样的地方在于，jsonp需要额外插入一个自己的回调方法
        const url = buildURL(
            fullPath,
            {
                ...config.params,
                ...{ [config.jsonpCallback || 'callback']: callbackName },
            },
            config.paramsSerializer
        );

        // 创建一个script标签
        let script = document.createElement('script');

        // 成功执行操作后
        function remove() {
            if (script) {
                script.onload = script.onerror = null;

                // 移除script标签
                if (script.parentNode) {
                    script.parentNode.removeChild(script);
                }
                // 取消定时器
                if (timer) {
                    clearTimeout(timer);
                }

                script = null;
            }
        }

        // 成功请求后
        window[callbackName] = (data) => {
            // 若已需要请求，则不再执行
            if (isAbort) {
                return;
            }

            // 返回的格式
            const response = {
                status: 200,
                statusText: 'ok',
                config,
                request: script,
                data: data,
            };
            remove();
            // 实际上这里上一个settle操作，会额外判断是否是合理的status状态
            // 若我们在config.validateStatus中设置404是合理的，也会进入到resolve状态
            // 但我们这里就不实现这个了
            // settle(resolve, reject, response);
            resolve(response);
        };

        // 请求失败
        script.onerror = function (error) {
            remove();

            reject(createError('Network Error', config, 404));
        };

        // 若设置了超时时间
        if (config.timeout) {
            timer = setTimeout(function () {
                remove();
                // 取消当前操作
                isAbort = true;
                reject(
                    createError(
                        'timeout of ' + config.timeout + 'ms exceeded',
                        config,
                        405
                    )
                );
            }, config.timeout);
        }

        // 若定义了取消操作
        if (config.cancelToken) {
            config.cancelToken.promise.then(function () {
                if (!script) {
                    return;
                }
                remove();
                isAbort = true;

                reject(createError('Cancel Error', config, 404));
            });
        }

        script.src = url;
        const target =
            document.getElementsByTagName('script')[0] || document.head;
        target.parentNode && target.parentNode.insertBefore(script, target);
    });
};

export default jsonpAdapter;
3. 将适配器添加到 axios 中
axios 的 config 提供了 adapter 字段让我们插入自己的适配器。使用自定义适配器又有两种情况：

完全只使用自定义的适配器；
在某种情况下使用自定义适配器，其他情况时还是使用 axios 自己的适配器。
第 1 种情况还好，只需要 return 自己适配器返回的结果结果即可；而第 2 种情况中，则有个小坑需要踩一下，我们这里也只讲解下第 2 种情况。我要把刚才实现的 jsonp 适配器添加到 axios 中，并且只在参数有format=jsonp时才调用该适配器，其他还是用的 axios 提供的适配器。

import Axios from 'axios';
import jsonpAdapter from './jsonpAdater';

const request = Axios.create({
    adapter: (config) => {
        if (config?.params?.format === 'jsonp') {
            return jsonpAdapter(config);
        }

        // 这里需要将config.adapter设置为空
        // 否则会造成无限循环
        return defaultAxios({ ...config, ...{ adapter: undefined } });
    },
});
使用方式，点击查看 demo【axios 自定义的 jsonp 适配器】：

使用自定义的适配器 jsonp 发起请求。

// 使用自定义的适配器jsonp发起请求
var options = {
    params: {
        format: 'jsonp',
    },
};
request(
    'https://api.prize.qq.com/v1/newsapp/answer/share/oneQ?qID=506336',
    options
)
    .then(function (response) {
        console.log('jsonp response', response);
    })
    .catch(function (error) {
        console.error('jsonp error', error);
    });
使用 axios 默认的适配器发起请求。

// 使用axios默认的适配器发起请求
request('https://api.prize.qq.com/v1/newsapp/answer/share/oneQ?qID=506336')
    .then(function (response) {
        console.log('axios response', response);
    })
    .catch(function (error) {
        console.error('axios error', error);
    });
4. 总结
这里，我们就已经实现了一个自定义适配器了，在满足一定条件时可以触发这个适配器。通过这个思路，我们也可以实现一个自定义的 mock 方法，例如当参数中包含format=mock时则调用 mock 接口，否则就正常请求。

标签：axios
阅读(4790)
No data
相关文章
axios 源码系列之拦截器的实现(2020/11/14)
必然会用到的 axios 中自带的工具方法(2020/11/17)
axios 源码系列之如何取消请求(2020/11/25)
真没必要再对 axios 进行过度的封装(2022/03/18)
如何避免旧请求的数据覆盖掉最新请求(2024/06/03)
如何合并同一接口的相同参数的请求(2024/09/13)
公众号
qrcode
微信公众号：前端小茶馆

1. 适配器要实现的功能
1.1 promise 和工具
1.2 响应的格式
1.3 超时设置
1.4 主动取消请求
2. 编写自定义适配器
3. 将适配器添加到 axios 中
4. 总结
查看评论
回到顶部
蚊子前端博客 ©2024
|
编程前沿导航
RSS
|
京ICP备16039464号
package com.squirrel.michale;

import java.util.Arrays;

/**
 * @author guanhao 观浩
 * @version 1.0.0.0
 * @createTime 2024/6/1 20:59
 * @company Michale Squirrel
 * @link
 * @description
 */
public class Day20240601_LeetCode000741_02 {

    public int cherryPickup(int[][] grid) {
        int n = grid.length;
        int[][][] dp = new int[2*n - 1][n][n];
        for (int[][] m : dp) {
            for (int[] r : m) {
                Arrays.fill(r, Integer.MIN_VALUE);
            }
        }
        dp[0][0][0] = grid[0][0];
        for (int k = 1; k < n * 2 - 1; k++) {
            for (int i1 = 0; i1 < n; i1++) {
                for (int i2 = 0; i2 < n; i2++) {
                    int j1 = k-i1;
                    int j2 = k-i2;
                    if (j1 < 0 || j1 >= n || j2 < 0 || j2 >= n || grid[i1][j1] == -1 || grid[i2][j2] == -1) {
                        continue;
                    }
                    int cherryCount = grid[i1][j1];
                     if (i1!=i2){
                         cherryCount += grid[i2][j2];
                     }
                    for (int x1 = i1-1; x1 <=i1; x1++) {
                        for (int x2 = i2-1; x2 <= i2; x2++) {
                            if (x1 >= 0 && x2 >= 0) {
                                dp[k][i1][i2] = Math.max(dp[k][i1][i2], dp[k - 1][x1][x2]);
                            }
                        }
                    }
                    dp[k][i1][i2] += cherryCount;
                }
            }
        }
        return Math.max(dp[n * 2 - 2][n-1][n-1], 0);
    }

    public static void main(String[] args) {
        int[][] grid = new int[][]{{0, 1, -1}, {1, 0, -1}, {1, 1, 1}};
        Day20240601_LeetCode000741_02 leetCode00074102 = new Day20240601_LeetCode000741_02();

        int i = leetCode00074102.cherryPickup(grid);
        System.out.println(i);


        int[][] grid2 = new int[][]{{1, 1, 1, 1, 0, 0, 0}, {0, 0, 0, 1, 0, 0, 0}, {0, 0, 0, 1, 0, 0, 1}, {1, 0, 0, 1, 0, 0, 0}, {0, 0, 0, 1, 0, 0, 0}, {0, 0, 0, 1, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 1}};


        int i2 = leetCode00074102.cherryPickup(grid2);
        System.out.println(i2);


    }

    Wenzi
首页
归档
标签
微说
关于
如何实现 axios 的自定义适配器 adapter
蚊子前端博客
发布于 2020/10/22 10:43
我们想基于 axios 扩展一些自己的数据请求方式（例如 mock 数据，某些 APP 内专属的数据请求方式等），并能够使用上 axios 提供的便捷功能，该怎么自定义一个适配器 adapter
Axios 是一个非常优秀的基于 promise 的 HTTP 库，可以用在浏览器和 node.js 中。并且提供了很多便捷的功能，例如：

支持 Promise API
拦截请求和响应
转换请求数据和响应数据
取消请求
自动转换 JSON 数据
客户端支持防御 XSRF
但如果我们想基于 axios 扩展一些自己的数据请求方式（例如 mock 数据，某些 APP 内专属的数据请求方式等），并能够使用上 axios 提供的便捷功能，该怎么自定义一个适配器 adapter；

1. 适配器要实现的功能
我们在基于 axios 实现额外的数据模块时，应当与 axios 的模式进行对齐。因此在返回的数据格式上，实现的功能上尽量保持一致。

1.1 promise 和工具
所有的适配均应当实现为 Promise 方式。

而且，有些功能的实现，axios 将其下放到了适配器中自己进行实现，例如

url 的拼接：即 baseURL 和 url 的拼接，若存在 baseURL 且 url 为相对路径，则进行拼接，否则直接使用 url；
参数的拼接：若是 get 请求，需要自行将 object 类型拼接为 url 参数的格式并与 url 拼接完成；
这是自己需要实现的两个基本的工具方法。

1.2 响应的格式
这里我们要注意到请求接口正常和异常的格式。

接口正常时：

const result = {
    status: 200, // 接口的http 状态
    statusText: 'ok',
    config: 'config', // 传入的config配置，原样返回即可，方便在响应拦截器和响应结果中使用
    data: {}, // 真实的接口返回结果
};
接口异常时，我们可以看下 axios 源码中对错误信息的处理createError，enhanceError（createError 中调用了 enhanceError），首先会创建一个 error 实例，然后给这个 error 实例添加一个属性：

module.exports = function enhanceError(error, config, code, request, response) {
    error.config = config;
    if (code) {
        error.code = code;
    }

    error.request = request;
    error.response = response;
    error.isAxiosError = true;

    error.toJSON = function toJSON() {
        return {
            // Standard
            message: this.message,
            name: this.name,
            // Microsoft
            description: this.description,
            number: this.number,
            // Mozilla
            fileName: this.fileName,
            lineNumber: this.lineNumber,
            columnNumber: this.columnNumber,
            stack: this.stack,
            // Axios
            config: this.config,
            code: this.code,
        };
    };
    return error;
};
可以看到，除了正常的错误信息外，还加入了很多别的属性，例如 request, response, config 等。这里我们在自己实现适配器时，最好也要这样统一编写，方便更上层的业务层统一处理，避免为单独的适配器进行特殊处理。

关于 1.1 和 1.2 中的内容，若不进行打包编译，则需要自己实现。若还要通过 webpack 等打包工具编译一下的，可以直接引用 axios 中的方法，不用自己实现了，参考官方基于 axios 实现的mock-axios。例如：

import axios from 'axios';
import buildURL from 'axios/lib/helpers/buildURL';
import isURLSameOrigin from 'axios/lib/helpers/isURLSameOrigin';
import btoa from 'axios/lib/helpers/btoa';
import cookies from 'axios/lib/helpers/cookies';
import settle from 'axios/lib/core/settle';
import createError from 'axios/lib/core/createError';
然后直接使用就行了，不用再进行二次开发。

1.3 超时设置
我们不能无限地等待第三方服务的响应，如果第三方服务无响应或者响应时间过长，应当适时的终止掉。在 axios 中，前端使用了XMLHttpRequest，在 node 端使用了http，来实现接口的请求，两者都有超时的设定，可以设置 timeout 字段来设置超时的时间，自动取消当前的请求。

像有的发起的请求，自己并没有超时的设定，例如 jsonp，是用创建一个 script 标签来发起的请求，这个请求必须等到服务器有响应才会终止（成功或者失败）。这时，就需要我们自己用一个setTimeout来模拟了，但这样，即使返回给业务层说“超时了，已取消当前请求”，但实际上请求还在，只不过若超过规定时间，只是不再执行对应的成功操作而已。

1.4 主动取消请求
我们也会有很多并没有到超时时间，就需要主动取消当前请求的场景，例如在请求返回之前就切换了路由；上次请求还没响应前，又需要发出新的请求等。都需要主动地取消当前请求。

axios 中已经提供了取消请求的功能，我们只需要按照规则接入即可。我们来看下 XMLHttpRequest 请求器中是怎么取消请求的，在写自定义请求器时也可以照理使用。

在lib/adapters/xhr.js#L158中：

// 若config中已经配置了cancelToken
if (config.cancelToken) {
    // Handle cancellation
    // 若在外城执行了取消请求的方法，则这里将当前的请求取消掉
    config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
            return;
        }

        // xhr中使用abort方法取消当前请求
        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
    });
}
我们在写自己的适配器时，也可以将这段拷贝过去，将内部取消的操作更换为自己的即可。

关于 cancel 操作执行的原理，这里暂不展开，有兴趣的可以参考这篇文章：axios cancelToken 原理解析。

到这里，若把上面的功能都实现了，就已经完成了一个标准的适配器了。

2. 编写自定义适配器
每个人需要的适配器肯定也不一样，复杂度也不一样，例如有的想接入小程序的请求，我自己想接入客户端里提供的数据请求方式等。我们这里只是通过实现一个简单的jsonp适配器来讲解下实现方式。

我们以 es6 的模块方式来进行开发。所有的实现均在代码中进行了讲解。

// 这里的config是axios里所有的配置
const jsonpAdapter = (config) => {
    return new Promise((resolve, reject) => {
        // 是否已取消当前操作
        // 因jsonp没有主动取消请求的方式
        // 这里使用 isAbort 来标识
        let isAbort = false;

        // 定时器标识符
        let timer = null;

        // 执行方法的名字，
        const callbackName = `jsonp${Date.now()}_${Math.random()
            .toString()
            .slice(2)}`;

        // 这里假设已经实现了baseURL和url的拼接方法
        const fullPath = buildFullPath(config.baseURL, config.url);

        // 这里假设已经实现了url和参数的拼接方法
        // 不太一样的地方在于，jsonp需要额外插入一个自己的回调方法
        const url = buildURL(
            fullPath,
            {
                ...config.params,
                ...{ [config.jsonpCallback || 'callback']: callbackName },
            },
            config.paramsSerializer
        );

        // 创建一个script标签
        let script = document.createElement('script');

        // 成功执行操作后
        function remove() {
            if (script) {
                script.onload = script.onerror = null;

                // 移除script标签
                if (script.parentNode) {
                    script.parentNode.removeChild(script);
                }
                // 取消定时器
                if (timer) {
                    clearTimeout(timer);
                }

                script = null;
            }
        }

        // 成功请求后
        window[callbackName] = (data) => {
            // 若已需要请求，则不再执行
            if (isAbort) {
                return;
            }

            // 返回的格式
            const response = {
                status: 200,
                statusText: 'ok',
                config,
                request: script,
                data: data,
            };
            remove();
            // 实际上这里上一个settle操作，会额外判断是否是合理的status状态
            // 若我们在config.validateStatus中设置404是合理的，也会进入到resolve状态
            // 但我们这里就不实现这个了
            // settle(resolve, reject, response);
            resolve(response);
        };

        // 请求失败
        script.onerror = function (error) {
            remove();

            reject(createError('Network Error', config, 404));
        };

        // 若设置了超时时间
        if (config.timeout) {
            timer = setTimeout(function () {
                remove();
                // 取消当前操作
                isAbort = true;
                reject(
                    createError(
                        'timeout of ' + config.timeout + 'ms exceeded',
                        config,
                        405
                    )
                );
            }, config.timeout);
        }

        // 若定义了取消操作
        if (config.cancelToken) {
            config.cancelToken.promise.then(function () {
                if (!script) {
                    return;
                }
                remove();
                isAbort = true;

                reject(createError('Cancel Error', config, 404));
            });
        }

        script.src = url;
        const target =
            document.getElementsByTagName('script')[0] || document.head;
        target.parentNode && target.parentNode.insertBefore(script, target);
    });
};

export default jsonpAdapter;
3. 将适配器添加到 axios 中
axios 的 config 提供了 adapter 字段让我们插入自己的适配器。使用自定义适配器又有两种情况：

完全只使用自定义的适配器；
在某种情况下使用自定义适配器，其他情况时还是使用 axios 自己的适配器。
第 1 种情况还好，只需要 return 自己适配器返回的结果结果即可；而第 2 种情况中，则有个小坑需要踩一下，我们这里也只讲解下第 2 种情况。我要把刚才实现的 jsonp 适配器添加到 axios 中，并且只在参数有format=jsonp时才调用该适配器，其他还是用的 axios 提供的适配器。

import Axios from 'axios';
import jsonpAdapter from './jsonpAdater';

const request = Axios.create({
    adapter: (config) => {
        if (config?.params?.format === 'jsonp') {
            return jsonpAdapter(config);
        }

        // 这里需要将config.adapter设置为空
        // 否则会造成无限循环
        return defaultAxios({ ...config, ...{ adapter: undefined } });
    },
});
使用方式，点击查看 demo【axios 自定义的 jsonp 适配器】：

使用自定义的适配器 jsonp 发起请求。

// 使用自定义的适配器jsonp发起请求
var options = {
    params: {
        format: 'jsonp',
    },
};
request(
    'https://api.prize.qq.com/v1/newsapp/answer/share/oneQ?qID=506336',
    options
)
    .then(function (response) {
        console.log('jsonp response', response);
    })
    .catch(function (error) {
        console.error('jsonp error', error);
    });
使用 axios 默认的适配器发起请求。

// 使用axios默认的适配器发起请求
request('https://api.prize.qq.com/v1/newsapp/answer/share/oneQ?qID=506336')
    .then(function (response) {
        console.log('axios response', response);
    })
    .catch(function (error) {
        console.error('axios error', error);
    });
4. 总结
这里，我们就已经实现了一个自定义适配器了，在满足一定条件时可以触发这个适配器。通过这个思路，我们也可以实现一个自定义的 mock 方法，例如当参数中包含format=mock时则调用 mock 接口，否则就正常请求。

标签：axios
阅读(4790)
No data
相关文章
axios 源码系列之拦截器的实现(2020/11/14)
必然会用到的 axios 中自带的工具方法(2020/11/17)
axios 源码系列之如何取消请求(2020/11/25)
真没必要再对 axios 进行过度的封装(2022/03/18)
如何避免旧请求的数据覆盖掉最新请求(2024/06/03)
如何合并同一接口的相同参数的请求(2024/09/13)
公众号
qrcode
微信公众号：前端小茶馆

1. 适配器要实现的功能
1.1 promise 和工具
1.2 响应的格式
1.3 超时设置
1.4 主动取消请求
2. 编写自定义适配器
3. 将适配器添加到 axios 中
4. 总结
查看评论
回到顶部
蚊子前端博客 ©2024
|
编程前沿导航
RSS
|
京ICP备16039464号

}

Wenzi
首页
归档
标签
微说
关于
如何实现 axios 的自定义适配器 adapter
蚊子前端博客
发布于 2020/10/22 10:43
我们想基于 axios 扩展一些自己的数据请求方式（例如 mock 数据，某些 APP 内专属的数据请求方式等），并能够使用上 axios 提供的便捷功能，该怎么自定义一个适配器 adapter
Axios 是一个非常优秀的基于 promise 的 HTTP 库，可以用在浏览器和 node.js 中。并且提供了很多便捷的功能，例如：

支持 Promise API
拦截请求和响应
转换请求数据和响应数据
取消请求
自动转换 JSON 数据
客户端支持防御 XSRF
但如果我们想基于 axios 扩展一些自己的数据请求方式（例如 mock 数据，某些 APP 内专属的数据请求方式等），并能够使用上 axios 提供的便捷功能，该怎么自定义一个适配器 adapter；

1. 适配器要实现的功能
我们在基于 axios 实现额外的数据模块时，应当与 axios 的模式进行对齐。因此在返回的数据格式上，实现的功能上尽量保持一致。

1.1 promise 和工具
所有的适配均应当实现为 Promise 方式。

而且，有些功能的实现，axios 将其下放到了适配器中自己进行实现，例如

url 的拼接：即 baseURL 和 url 的拼接，若存在 baseURL 且 url 为相对路径，则进行拼接，否则直接使用 url；
参数的拼接：若是 get 请求，需要自行将 object 类型拼接为 url 参数的格式并与 url 拼接完成；
这是自己需要实现的两个基本的工具方法。

1.2 响应的格式
这里我们要注意到请求接口正常和异常的格式。

接口正常时：

const result = {
    status: 200, // 接口的http 状态
    statusText: 'ok',
    config: 'config', // 传入的config配置，原样返回即可，方便在响应拦截器和响应结果中使用
    data: {}, // 真实的接口返回结果
};
接口异常时，我们可以看下 axios 源码中对错误信息的处理createError，enhanceError（createError 中调用了 enhanceError），首先会创建一个 error 实例，然后给这个 error 实例添加一个属性：

module.exports = function enhanceError(error, config, code, request, response) {
    error.config = config;
    if (code) {
        error.code = code;
    }

    error.request = request;
    error.response = response;
    error.isAxiosError = true;

    error.toJSON = function toJSON() {
        return {
            // Standard
            message: this.message,
            name: this.name,
            // Microsoft
            description: this.description,
            number: this.number,
            // Mozilla
            fileName: this.fileName,
            lineNumber: this.lineNumber,
            columnNumber: this.columnNumber,
            stack: this.stack,
            // Axios
            config: this.config,
            code: this.code,
        };
    };
    return error;
};
可以看到，除了正常的错误信息外，还加入了很多别的属性，例如 request, response, config 等。这里我们在自己实现适配器时，最好也要这样统一编写，方便更上层的业务层统一处理，避免为单独的适配器进行特殊处理。

关于 1.1 和 1.2 中的内容，若不进行打包编译，则需要自己实现。若还要通过 webpack 等打包工具编译一下的，可以直接引用 axios 中的方法，不用自己实现了，参考官方基于 axios 实现的mock-axios。例如：

import axios from 'axios';
import buildURL from 'axios/lib/helpers/buildURL';
import isURLSameOrigin from 'axios/lib/helpers/isURLSameOrigin';
import btoa from 'axios/lib/helpers/btoa';
import cookies from 'axios/lib/helpers/cookies';
import settle from 'axios/lib/core/settle';
import createError from 'axios/lib/core/createError';
然后直接使用就行了，不用再进行二次开发。

1.3 超时设置
我们不能无限地等待第三方服务的响应，如果第三方服务无响应或者响应时间过长，应当适时的终止掉。在 axios 中，前端使用了XMLHttpRequest，在 node 端使用了http，来实现接口的请求，两者都有超时的设定，可以设置 timeout 字段来设置超时的时间，自动取消当前的请求。

像有的发起的请求，自己并没有超时的设定，例如 jsonp，是用创建一个 script 标签来发起的请求，这个请求必须等到服务器有响应才会终止（成功或者失败）。这时，就需要我们自己用一个setTimeout来模拟了，但这样，即使返回给业务层说“超时了，已取消当前请求”，但实际上请求还在，只不过若超过规定时间，只是不再执行对应的成功操作而已。

1.4 主动取消请求
我们也会有很多并没有到超时时间，就需要主动取消当前请求的场景，例如在请求返回之前就切换了路由；上次请求还没响应前，又需要发出新的请求等。都需要主动地取消当前请求。

axios 中已经提供了取消请求的功能，我们只需要按照规则接入即可。我们来看下 XMLHttpRequest 请求器中是怎么取消请求的，在写自定义请求器时也可以照理使用。

在lib/adapters/xhr.js#L158中：

// 若config中已经配置了cancelToken
if (config.cancelToken) {
    // Handle cancellation
    // 若在外城执行了取消请求的方法，则这里将当前的请求取消掉
    config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
            return;
        }

        // xhr中使用abort方法取消当前请求
        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
    });
}
我们在写自己的适配器时，也可以将这段拷贝过去，将内部取消的操作更换为自己的即可。

关于 cancel 操作执行的原理，这里暂不展开，有兴趣的可以参考这篇文章：axios cancelToken 原理解析。

到这里，若把上面的功能都实现了，就已经完成了一个标准的适配器了。

2. 编写自定义适配器
每个人需要的适配器肯定也不一样，复杂度也不一样，例如有的想接入小程序的请求，我自己想接入客户端里提供的数据请求方式等。我们这里只是通过实现一个简单的jsonp适配器来讲解下实现方式。

我们以 es6 的模块方式来进行开发。所有的实现均在代码中进行了讲解。

// 这里的config是axios里所有的配置
const jsonpAdapter = (config) => {
    return new Promise((resolve, reject) => {
        // 是否已取消当前操作
        // 因jsonp没有主动取消请求的方式
        // 这里使用 isAbort 来标识
        let isAbort = false;

        // 定时器标识符
        let timer = null;

        // 执行方法的名字，
        const callbackName = `jsonp${Date.now()}_${Math.random()
            .toString()
            .slice(2)}`;

        // 这里假设已经实现了baseURL和url的拼接方法
        const fullPath = buildFullPath(config.baseURL, config.url);

        // 这里假设已经实现了url和参数的拼接方法
        // 不太一样的地方在于，jsonp需要额外插入一个自己的回调方法
        const url = buildURL(
            fullPath,
            {
                ...config.params,
                ...{ [config.jsonpCallback || 'callback']: callbackName },
            },
            config.paramsSerializer
        );

        // 创建一个script标签
        let script = document.createElement('script');

        // 成功执行操作后
        function remove() {
            if (script) {
                script.onload = script.onerror = null;

                // 移除script标签
                if (script.parentNode) {
                    script.parentNode.removeChild(script);
                }
                // 取消定时器
                if (timer) {
                    clearTimeout(timer);
                }

                script = null;
            }
        }

        // 成功请求后
        window[callbackName] = (data) => {
            // 若已需要请求，则不再执行
            if (isAbort) {
                return;
            }

            // 返回的格式
            const response = {
                status: 200,
                statusText: 'ok',
                config,
                request: script,
                data: data,
            };
            remove();
            // 实际上这里上一个settle操作，会额外判断是否是合理的status状态
            // 若我们在config.validateStatus中设置404是合理的，也会进入到resolve状态
            // 但我们这里就不实现这个了
            // settle(resolve, reject, response);
            resolve(response);
        };

        // 请求失败
        script.onerror = function (error) {
            remove();

            reject(createError('Network Error', config, 404));
        };

        // 若设置了超时时间
        if (config.timeout) {
            timer = setTimeout(function () {
                remove();
                // 取消当前操作
                isAbort = true;
                reject(
                    createError(
                        'timeout of ' + config.timeout + 'ms exceeded',
                        config,
                        405
                    )
                );
            }, config.timeout);
        }

        // 若定义了取消操作
        if (config.cancelToken) {
            config.cancelToken.promise.then(function () {
                if (!script) {
                    return;
                }
                remove();
                isAbort = true;

                reject(createError('Cancel Error', config, 404));
            });
        }

        script.src = url;
        const target =
            document.getElementsByTagName('script')[0] || document.head;
        target.parentNode && target.parentNode.insertBefore(script, target);
    });
};

export default jsonpAdapter;
3. 将适配器添加到 axios 中
axios 的 config 提供了 adapter 字段让我们插入自己的适配器。使用自定义适配器又有两种情况：

完全只使用自定义的适配器；
在某种情况下使用自定义适配器，其他情况时还是使用 axios 自己的适配器。
第 1 种情况还好，只需要 return 自己适配器返回的结果结果即可；而第 2 种情况中，则有个小坑需要踩一下，我们这里也只讲解下第 2 种情况。我要把刚才实现的 jsonp 适配器添加到 axios 中，并且只在参数有format=jsonp时才调用该适配器，其他还是用的 axios 提供的适配器。

import Axios from 'axios';
import jsonpAdapter from './jsonpAdater';

const request = Axios.create({
    adapter: (config) => {
        if (config?.params?.format === 'jsonp') {
            return jsonpAdapter(config);
        }

        // 这里需要将config.adapter设置为空
        // 否则会造成无限循环
        return defaultAxios({ ...config, ...{ adapter: undefined } });
    },
});
使用方式，点击查看 demo【axios 自定义的 jsonp 适配器】：

使用自定义的适配器 jsonp 发起请求。

// 使用自定义的适配器jsonp发起请求
var options = {
    params: {
        format: 'jsonp',
    },
};
request(
    'https://api.prize.qq.com/v1/newsapp/answer/share/oneQ?qID=506336',
    options
)
    .then(function (response) {
        console.log('jsonp response', response);
    })
    .catch(function (error) {
        console.error('jsonp error', error);
    });
使用 axios 默认的适配器发起请求。

// 使用axios默认的适配器发起请求
request('https://api.prize.qq.com/v1/newsapp/answer/share/oneQ?qID=506336')
    .then(function (response) {
        console.log('axios response', response);
    })
    .catch(function (error) {
        console.error('axios error', error);
    });
4. 总结
这里，我们就已经实现了一个自定义适配器了，在满足一定条件时可以触发这个适配器。通过这个思路，我们也可以实现一个自定义的 mock 方法，例如当参数中包含format=mock时则调用 mock 接口，否则就正常请求。

标签：axios
阅读(4790)
No data
相关文章
axios 源码系列之拦截器的实现(2020/11/14)
必然会用到的 axios 中自带的工具方法(2020/11/17)
axios 源码系列之如何取消请求(2020/11/25)
真没必要再对 axios 进行过度的封装(2022/03/18)
如何避免旧请求的数据覆盖掉最新请求(2024/06/03)
如何合并同一接口的相同参数的请求(2024/09/13)
公众号
qrcode
微信公众号：前端小茶馆

1. 适配器要实现的功能
1.1 promise 和工具
1.2 响应的格式
1.3 超时设置
1.4 主动取消请求
2. 编写自定义适配器
3. 将适配器添加到 axios 中
4. 总结
查看评论
回到顶部
蚊子前端博客 ©2024
|
编程前沿导航
RSS
|
京ICP备16039464号

